// MIT License
//
// Copyright (c) 2023-Present - Violet Hansen - (aka HotCakeX on GitHub) - Email Address: spynetgirl@outlook.com
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// See here for more information: https://github.com/HotCakeX/Harden-Windows-Security/blob/main/LICENSE
//

using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using CommonCore.ThermalMonitors;
using Microsoft.UI.Dispatching;
using Microsoft.Win32;

namespace AppControlManager.ViewModels;

internal sealed partial class HomeVM : ViewModelBase, IDisposable
{
	/// <summary>
	/// Event handler for when the home page is loaded.
	/// We do not auto-start the heavy edge pulse storyboard, instead we rasterize and pulse in code.
	/// Glitch storyboards are short, single-run bursts and are triggered by the page every N seconds.
	/// </summary>
	/// <param name="sender"></param>
	internal void OnHomePageLoaded(object sender)
	{
		// Let these finish without waiting for them.
		_ = Task.Run(() =>
		{
			try
			{
				// Initialize and start the minute-aligned clock updater that only fires when the minute changes.
				InitializeSystemTimeUpdater();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				// Initialize and start the app RAM usage updater that fires every 2 seconds.
				InitializeAppRamUpdater();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				// Initialize CPU temperature sampler
				InitializeCpuTemperatureSampler();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				// Initialize and start the internet speed updater that fires every 2 second.
				InitializeInternetSpeedUpdater();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				// Refresh the Windows Defender feed info asynchronously (fire-and-forget)
				_ = RefreshDefenderFeedAsync();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				CpuDetailsText = GetCpuDetailsString();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				TimeZoneText = GetLocalUtcOffsetString();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				UptimeText = GetSystemUptimeString();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				SystemRamText = GetPhysicalMemoryExtendedString();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				DiskSizeText = GetTotalPhysicalDiskSizeString();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{

				OsInfoText = GetOsDetailsString();
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				ComputerNameText = Environment.MachineName;
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

			try
			{
				UserKindText = Environment.IsPrivilegedProcess ? $"{Environment.UserName} - Administrator Privilege" : $"{Environment.UserName} - Standard Privilege";
			}
			catch (Exception ex)
			{
				Logger.Write(ex);
			}

		});
	}

	/// <summary>
	/// Runs when the Home page is unloaded. Called by the page's code behind since it's the one using x:Bind in the XAML.
	/// The active timers and data retrievals should not run in background when user is not on Home page.
	/// </summary>
	internal void OnHomePageUnLoaded()
	{
		// Clean up the clock timer
		if (_clockTimer is not null)
		{
			_clockTimer.Stop();
			_clockTimer.Tick -= OnClockInitialTick;
			_clockTimer.Tick -= OnClockTick;
			_clockTimer = null;
		}

		// Clean up the app RAM usage timer
		if (_appRamTimer is not null)
		{
			_appRamTimer.Stop();
			_appRamTimer.Tick -= OnAppRamTick;
			_appRamTimer = null;
		}

		// Dispose CPU temperature sampler if active
		_temperatureSampler?.Dispose();
		_temperatureSampler = null;
	}

	// Textblock sources bound to the UI for system info HUDs.
	internal string? SystemTimeText { get; private set => SP(ref field, value); }
	internal string? TimeZoneText { get; private set => SP(ref field, value); }
	internal string? UserKindText { get; private set => SP(ref field, value); }
	internal string? UptimeText { get; private set => SP(ref field, value); }
	internal string? SystemRamText { get; private set => SP(ref field, value); }
	internal string? AppRamText { get; private set => SP(ref field, value); }
	internal string? DiskSizeText { get; private set => SP(ref field, value); }
	internal string? InternetSpeedText { get; private set => SP(ref field, value); }
	internal string? InternetTotalText { get; private set => SP(ref field, value); } = "Total: 0.0 GB ↓ / 0.0 GB ↑";
	internal string? CpuTemperatureText { get; private set => SP(ref field, value); } = "CPU Temp: Unavailable";
	internal string? OsInfoText { get; private set => SP(ref field, value); }
	internal string? CpuDetailsText { get; private set => SP(ref field, value); }
	internal string? ComputerNameText { get; private set => SP(ref field, value); }

	/// <summary>
	/// Timer first used as one-shot to align to next minute, then switched to repeating every minute.
	/// </summary>
	private DispatcherQueueTimer? _clockTimer;

	/// <summary>
	/// Timer that updates the app's working set (RAM usage).
	/// </summary>
	private DispatcherQueueTimer? _appRamTimer;

	/// <summary>
	/// CPU temperature sampler instance
	/// </summary>
	private TemperatureSampler? _temperatureSampler;

	/// <summary>
	/// Sets the current time immediately, then aligns the timer to the next minute boundary.
	/// After the first tick, the same timer becomes a repeating one-minute timer.
	/// </summary>
	private void InitializeSystemTimeUpdater()
	{
		try
		{
			// Always set the initial value immediately.
			UpdateSystemTime();

			// Compute due time until the next minute boundary.
			DateTime now = DateTime.Now;
			int msIntoMinute = now.Second * 1000 + now.Millisecond;
			TimeSpan due = TimeSpan.FromMilliseconds(60000 - msIntoMinute);

			_clockTimer = Dispatcher.CreateTimer();
			_clockTimer.IsRepeating = false; // one-shot to align to the next minute
			_clockTimer.Interval = due;
			_clockTimer.Tick += OnClockInitialTick;
			_clockTimer.Start();
		}
		catch (Exception ex)
		{
			Logger.Write(ex);
		}
	}

	/// <summary>
	/// Initializes and starts the repeating timer that updates the app RAM usage every 2 seconds.
	/// Also sets the initial value immediately.
	/// </summary>
	private void InitializeAppRamUpdater()
	{
		try
		{
			// Always set the initial value immediately.
			AppRamText = GetAppPrivateWorkingSetBytes_Native();

			_appRamTimer = Dispatcher.CreateTimer();
			_appRamTimer.IsRepeating = true; // repeating update
			_appRamTimer.Interval = TimeSpan.FromSeconds(2);
			_appRamTimer.Tick += OnAppRamTick;
			_appRamTimer.Start();
		}
		catch (Exception ex)
		{
			Logger.Write(ex);
		}
	}

	/// <summary>
	/// Initializes the CPU temperature sampler.
	/// </summary>
	private void InitializeCpuTemperatureSampler()
	{
		try
		{
			_temperatureSampler = new TemperatureSampler();
		}
		catch (Exception ex)
		{
			Logger.Write(ex);
		}
		UpdateCpuTemperature();
	}

	/// <summary>
	/// One-shot tick at the next minute boundary.
	/// Updates the time and switches the timer to repeating once per minute.
	/// </summary>
	private void OnClockInitialTick(DispatcherQueueTimer sender, object args)
	{
		sender.Tick -= OnClockInitialTick;

		// Show the exact minute we just hit.
		UpdateSystemTime();

		// Switch to repeating once per minute.
		sender.IsRepeating = true;
		sender.Interval = TimeSpan.FromMinutes(1);
		sender.Tick += OnClockTick;

		// Restart after converting to repeating.
		sender.Start();
	}

	/// <summary>
	/// Repeating minute timer tick updates the time text.
	/// </summary>
	private void OnClockTick(DispatcherQueueTimer sender, object args) => UpdateSystemTime();

	/// <summary>
	/// Repeating 2-second timer tick updates the app RAM usage and Internet speed.
	/// Also updates CPU temperature.
	/// </summary>
	private void OnAppRamTick(DispatcherQueueTimer sender, object args)
	{
		AppRamText = GetAppPrivateWorkingSetBytes_Native();
		UpdateInternetSpeed(first: false);
		UpdateCpuTemperature();
	}

	/// <summary>
	/// Formats and sets the current system time text with hour and minute only.
	/// Respects system 12/24-hour preference.
	/// </summary>
	private void UpdateSystemTime()
	{
		DateTime now = DateTime.Now;
		DateTimeFormatInfo dfi = CultureInfo.CurrentCulture.DateTimeFormat;

		// Detect 24-hour vs 12-hour from the current culture's short time pattern.
		bool is24Hour = dfi.ShortTimePattern.Contains('H', StringComparison.Ordinal);

		string format = is24Hour ? "HH:mm" : "h:mm tt";

		SystemTimeText = now.ToString(format, CultureInfo.CurrentCulture);
	}

	/// <summary>
	/// Samples current CPU temperature and updates bound text.
	/// </summary>
	private void UpdateCpuTemperature()
	{
		if (_temperatureSampler is null)
			return;

		double celsius = _temperatureSampler.SampleCelsiusOneShot();

		if (double.IsNaN(celsius) || double.IsInfinity(celsius))
			return;

		CpuTemperatureText = celsius.ToString("0.0", CultureInfo.InvariantCulture) + " °C";
	}

	/// <summary>
	/// Returns the local time zone's UTC offset as a string in "+HH:mm" or "-HH:mm" format.
	/// </summary>
	/// <returns>formatted string</returns>
	private static string GetLocalUtcOffsetString()
	{
		TimeSpan offset = DateTimeOffset.Now.Offset; // DST-aware local offset
		int totalMinutes = (int)Math.Round(offset.TotalMinutes, MidpointRounding.AwayFromZero);
		int absMinutes = Math.Abs(totalMinutes);
		int hours = absMinutes / 60;
		int minutes = absMinutes % 60;
		string sign = totalMinutes >= 0 ? "+" : "-";
		return FormattableString.Invariant($"{sign}{hours:00}:{minutes:00}");
	}

	/// <summary>
	/// Returns the System Uptime in a formatted string.
	/// </summary>
	/// <returns></returns>
	private static string GetSystemUptimeString()
	{
		// Monotonic uptime since system start; unaffected by manual clock changes.
		TimeSpan uptime = TimeSpan.FromMilliseconds(Environment.TickCount64);

		int days = uptime.Days;
		int hours = uptime.Hours;
		int minutes = uptime.Minutes;

		string result = days > 0
			? string.Format(CultureInfo.InvariantCulture, "{0}d {1:D2}h {2:D2}m", days, hours, minutes)
			: string.Format(CultureInfo.InvariantCulture, "{0:D2}h {1:D2}m", hours, minutes);

		return result;
	}

	/// <summary>
	/// Gets the available RAM on the system.
	/// </summary>
	/// <returns></returns>
	private static string GetAvailablePhysicalMemoryBytes()
	{
		bool ok = NativeMethods.GetPhysicallyInstalledSystemMemory(out ulong totalKilobytes);
		if (!ok || totalKilobytes == 0)
			return "0";

		ulong bytes = totalKilobytes * 1024UL; // KB -> bytes
		return ByteToString(bytes);
	}

	/// <summary>
	/// Returns the current process's Private Working Set (bytes).
	/// Falls back to Working Set when PrivateWorkingSetSize is not available.
	/// </summary>
	/// <returns>bytes</returns>
	private static string GetAppPrivateWorkingSetBytes_Native()
	{
		PROCESS_MEMORY_COUNTERS_EX2 counters = default;
		counters.cb = (uint)Unsafe.SizeOf<PROCESS_MEMORY_COUNTERS_EX2>();

		IntPtr hProcess = NativeMethods.GetCurrentProcess();
		bool ok = NativeMethods.K32GetProcessMemoryInfo(hProcess, ref counters, counters.cb);
		if (!ok)
		{
			return ByteToString(0UL);
		}

		// Prefer Private Working Set to match Task Manager's reported value; if zero/unavailable, fall back to total Working Set.
		ulong privateWs = counters.PrivateWorkingSetSize;
		if (privateWs != 0UL)
		{
			return ByteToString(privateWs);
		}

		return ByteToString(counters.WorkingSetSize);
	}

	[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
	private static string ByteToString(ulong bytes)
	{
		const ulong OneGB = 1024UL * 1024UL * 1024UL;
		const ulong OneMB = 1024UL * 1024UL;

		if (bytes >= OneGB)
		{
			double gb = bytes / (double)OneGB;
			return gb.ToString("0.#", CultureInfo.InvariantCulture) + " GB";
		}
		else
		{
			double mb = bytes / (double)OneMB;
			return mb.ToString("0.#", CultureInfo.InvariantCulture) + " MB";
		}
	}

	/// <summary>
	/// Returns the total combined size of all physical disks as a formatted string.
	/// Using managed code here so we can display this info in unelevated sessions as well.
	/// </summary>
	private static string GetTotalPhysicalDiskSizeString()
	{
		ulong totalBytes = 0UL;

		DriveInfo[] drives = DriveInfo.GetDrives();
		int count = drives.Length;
		for (int i = 0; i < count; i++)
		{
			DriveInfo drive = drives[i];

			// Only count fixed, ready volumes
			if (drive.DriveType == DriveType.Fixed && drive.IsReady)
			{
				long size = drive.TotalSize;
				if (size > 0)
				{
					totalBytes += (ulong)size;
				}
			}
		}

		return totalBytes == 0UL ? "0" : ByteToString(totalBytes);
	}

	// Cached interface index for the best route to the internet.
	private uint _netIfIndex;

	// 64-bit previous counters and timestamp to compute deltas.
	private ulong _prevInBytes;
	private ulong _prevOutBytes;
	private long _prevSampleTicks;

	/// <summary>
	/// Initializes and starts the internet speed (throughput) updater.
	/// Picks the interface Windows routes to 8.8.8.8 and computes bps from 64-bit octet deltas.
	/// </summary>
	private void InitializeInternetSpeedUpdater()
	{
		// Resolve the interface once at start; re-resolved automatically if needed later.
		_netIfIndex = ResolveBestInterfaceIndex();

		_prevInBytes = 0UL;
		_prevOutBytes = 0UL;
		_prevSampleTicks = 0;

		// Take an initial baseline sample (shows zeros first, as there's no prior delta yet).
		UpdateInternetSpeed(first: true);
	}

	/// <summary>
	/// Samples interface counters and updates InternetSpeedText as "X.X Mbps ↓ / Y.Y Mbps ↑".
	/// Uses 64-bit byte counters to avoid 32-bit wrap issues on high-speed links.
	/// Handles adapter changes and resets gracefully.
	/// </summary>
	private void UpdateInternetSpeed(bool first)
	{
		// If we do not have a valid interface index, try to resolve again.
		if (_netIfIndex == 0)
		{
			_netIfIndex = ResolveBestInterfaceIndex();

			if (_netIfIndex == 0)
			{
				InternetSpeedText = "0.0 Mbps ↓ / 0.0 Mbps ↑";
				InternetTotalText = "Total: 0 B ↓ / 0 B ↑";
				return;
			}
		}

		MIB_IF_ROW2 row = default;
		row.InterfaceIndex = _netIfIndex;

		uint result = NativeMethods.GetIfEntry2(ref row);

		// If call failed (like interface removed), try to re-resolve once.
		if (result != 0)
		{
			_netIfIndex = ResolveBestInterfaceIndex();
			if (_netIfIndex != 0)
			{
				row = default;
				row.InterfaceIndex = _netIfIndex;
				result = NativeMethods.GetIfEntry2(ref row);
			}
		}

		if (result != 0)
		{
			// Still failed
			InternetSpeedText = "0.0 Mbps ↓ / 0.0 Mbps ↑";
			InternetTotalText = "Total: 0 B ↓ / 0 B ↑";
			return;
		}

		long nowTicks = Environment.TickCount64;

		// First sample just seeds the baseline (no delta yet).
		if (first || _prevSampleTicks == 0)
		{
			_prevInBytes = row.InOctets;
			_prevOutBytes = row.OutOctets;
			_prevSampleTicks = nowTicks;
			InternetSpeedText = "0.0 Mbps ↓ / 0.0 Mbps ↑";
			InternetTotalText = "Total: " + FormatDataSize(_prevInBytes) + " ↓ / " + FormatDataSize(_prevOutBytes) + " ↑";
			return;
		}

		double elapsedSec = (nowTicks - _prevSampleTicks) / 1000.0;
		if (elapsedSec <= 0)
		{
			return;
		}

		ulong curIn = row.InOctets;
		ulong curOut = row.OutOctets;

		// Handle adapter reset (counters dropped), which manifests as a decreasing value even for 64-bit counters.
		if (curIn < _prevInBytes || curOut < _prevOutBytes)
		{
			_prevInBytes = curIn;
			_prevOutBytes = curOut;
			_prevSampleTicks = nowTicks;
			InternetSpeedText = "0.0 Mbps ↓ / 0.0 Mbps ↑";
			InternetTotalText = "Total: " + FormatDataSize(_prevInBytes) + " ↓ / " + FormatDataSize(_prevOutBytes) + " ↑";
			return;
		}

		ulong deltaIn = curIn - _prevInBytes;
		ulong deltaOut = curOut - _prevOutBytes;

		// Bytes/sec -> bits/sec, then format to human-readable Mbps/Gbps with one decimal.
		double bpsDown = deltaIn * 8.0 / elapsedSec;
		double bpsUp = deltaOut * 8.0 / elapsedSec;

		_prevInBytes = curIn;
		_prevOutBytes = curOut;
		_prevSampleTicks = nowTicks;

		InternetSpeedText = FormatBitrate(bpsDown) + " ↓ / " + FormatBitrate(bpsUp) + " ↑";
		InternetTotalText = "Total: " + FormatDataSize(curIn) + " ↓ / " + FormatDataSize(curOut) + " ↑";
	}

	/// <summary>
	/// Returns the Windows-selected interface index for reaching 8.8.8.8 (IPv4).
	/// </summary>
	private static uint ResolveBestInterfaceIndex()
	{
		// 8.8.8.8 in network byte order as a 32-bit IPv4 address: 0x08 0x08 0x08 0x08
		const uint destAddrNetworkOrder = (8 << 24) | (8 << 16) | (8 << 8) | 8;
		uint status = NativeMethods.GetBestInterface(destAddrNetworkOrder, out uint index);
		return status == 0 ? index : 0U; // 0 = NO_ERROR
	}

	/// <summary>
	/// Formats bits-per-second as "X.X Kbps/Mbps/Gbps" with one decimal.
	/// </summary>
	[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
	private static string FormatBitrate(double bps)
	{
		const double Kbps = 1000.0;
		const double Mbps = 1000.0 * 1000.0;
		const double Gbps = 1000.0 * 1000.0 * 1000.0;

		if (bps >= Gbps)
		{
			double val = bps / Gbps;
			return val.ToString("0.0", CultureInfo.InvariantCulture) + " Gbps";
		}
		else if (bps >= Mbps)
		{
			double val = bps / Mbps;
			return val.ToString("0.0", CultureInfo.InvariantCulture) + " Mbps";
		}
		else
		{
			double val = bps / Kbps;
			return val.ToString("0.0", CultureInfo.InvariantCulture) + " Kbps";
		}
	}

	/// <summary>
	/// Formats a cumulative byte counter using binary units (KiB, MiB, GiB, TiB) collapsed to KB/MB/GB/TB labels,
	/// with one decimal precision.
	/// For values below 1 KB it shows raw bytes without decimal.
	/// </summary>
	[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
	private static string FormatDataSize(ulong bytes)
	{
		const double OneKB = 1024.0;
		const double OneMB = OneKB * 1024.0;
		const double OneGB = OneMB * 1024.0;
		const double OneTB = OneGB * 1024.0;

		if (bytes >= (ulong)OneTB)
		{
			double val = bytes / OneTB;
			return val.ToString("0.0", CultureInfo.InvariantCulture) + " TB";
		}
		else if (bytes >= (ulong)OneGB)
		{
			double val = bytes / OneGB;
			return val.ToString("0.0", CultureInfo.InvariantCulture) + " GB";
		}
		else if (bytes >= (ulong)OneMB)
		{
			double val = bytes / OneMB;
			return val.ToString("0.0", CultureInfo.InvariantCulture) + " MB";
		}
		else if (bytes >= (ulong)OneKB)
		{
			double val = bytes / OneKB;
			return val.ToString("0.0", CultureInfo.InvariantCulture) + " KB";
		}
		else
		{
			// Show raw bytes for very small totals
			return bytes.ToString(CultureInfo.InvariantCulture) + " B";
		}
	}

	#region Defender Feed Fields

	internal string? EngineVersionText { get; private set => SP(ref field, value); } = "Antimalware engine version: Unavailable";
	internal string? SignatureVersionText { get; private set => SP(ref field, value); } = "Antivirus definition version: Unavailable";
	internal string? PlatformVersionText { get; private set => SP(ref field, value); } = "Platform version: Unavailable";
	internal string? SignatureUpdateDateText { get; private set => SP(ref field, value); } = "Definition update time: Unavailable";

	private static readonly Uri OnlineMSDefenderStatusURL = new("https://definitionupdates.microsoft.com/packages?action=info");

	private async Task RefreshDefenderFeedAsync(CancellationToken cancellationToken = default)
	{
		// Defaults for failure cases
		string engine = "Unavailable";
		string signatures = "Unavailable";
		string platform = "Unavailable";
		string dateText = "Unavailable";

		try
		{
			using HttpClient httpClient = new()
			{
				Timeout = TimeSpan.FromSeconds(8)
			};

			// Download XML

			using CancellationTokenSource timeoutCts = new(TimeSpan.FromSeconds(10));
			using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
			string xml = await httpClient.GetStringAsync(OnlineMSDefenderStatusURL, linked.Token)
										 .ConfigureAwait(false);

			// Parse
			XDocument doc = XDocument.Parse(xml, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
			XElement root = doc.Root!;
			if (root != null && string.Equals(root.Name.LocalName, "versions", StringComparison.OrdinalIgnoreCase))
			{
				string engineVal = root.Element(XName.Get("engine"))?.Value?.Trim() ?? string.Empty;
				string platformVal = root.Element(XName.Get("platform"))?.Value?.Trim() ?? string.Empty;

				XElement? sigEl = root.Element(XName.Get("signatures"));
				string signaturesVal = sigEl?.Value?.Trim() ?? string.Empty;
				string? dateAttr = sigEl?.Attribute(XName.Get("date"))?.Value;

				// Assign parsed or fallback
				engine = string.IsNullOrWhiteSpace(engineVal) ? "Unavailable" : engineVal;
				platform = string.IsNullOrWhiteSpace(platformVal) ? "Unavailable" : platformVal;
				signatures = string.IsNullOrWhiteSpace(signaturesVal) ? "Unavailable" : signaturesVal;

				if (!string.IsNullOrWhiteSpace(dateAttr))
				{
					// The feed gives "YYYY-MM-DD HH:MM:SSZ"
					if (DateTimeOffset.TryParse(dateAttr, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTimeOffset dto))
					{
						// Keep UTC and format compactly
						dateText = dto.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture);
					}
				}
			}
		}
		catch
		{
			// Network/parse failures fall back to "Unavailable" texts
		}

		EngineVersionText = $"Antimalware engine version: {engine}";
		SignatureVersionText = $"Antivirus definition version: {signatures}";
		PlatformVersionText = $"Platform version: {platform}";
		SignatureUpdateDateText = $"Definition update time: {dateText}";
	}

	#endregion


	#region CPU

	private static unsafe string GetCpuDetailsString()
	{
		string brand = "Unknown CPU";
		try
		{
			object? value = Registry.GetValue(@"HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0", "ProcessorNameString", null);
			if (value is string s && !string.IsNullOrWhiteSpace(s))
			{
				brand = s.Trim();
			}
		}
		catch
		{ }

		// Base clock
		string baseClock = "Base Clock N/A";
		try
		{
			object? mhzObj = Registry.GetValue(@"HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0", "~MHz", null);
			if (mhzObj is int mhz && mhz > 0)
			{
				double ghz = mhz / 1000.0;
				baseClock = ghz.ToString("0.##", CultureInfo.InvariantCulture) + " GHz";
			}
		}
		catch
		{ }

		string archText = RuntimeInformation.ProcessArchitecture.ToString();
		int logicalThreads = Environment.ProcessorCount;
		int physicalCores = 0;
		int packageCount = 0;
		ulong largestCacheBytes = 0UL;

		// Topology enumeration
		try
		{
			int len = 0;
			bool firstOk = NativeMethods.GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP.RelationAll, IntPtr.Zero, ref len);
			int firstErr = Marshal.GetLastPInvokeError();
			if (!firstOk && firstErr == 122 && len > 0) // ERROR_INSUFFICIENT_BUFFER
			{
				IntPtr buffer = Marshal.AllocHGlobal(len);
				try
				{
					if (NativeMethods.GetLogicalProcessorInformationEx(LOGICAL_PROCESSOR_RELATIONSHIP.RelationAll, buffer, ref len))
					{
						int offset = 0;
						while (offset < len)
						{
							int relationship = Marshal.ReadInt32(buffer, offset);
							int size = Marshal.ReadInt32(buffer, offset + 4);
							if (size <= 8 || offset + size > len)
							{
								break;
							}

							int dataOffset = offset + 8;
							if (dataOffset < 0 || dataOffset > len)
								break;

							IntPtr dataPtr = IntPtr.Add(buffer, dataOffset);
							switch (relationship)
							{
								case (int)LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore:
									physicalCores++;
									break;
								case (int)LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorPackage:
									packageCount++;
									break;
								case (int)LOGICAL_PROCESSOR_RELATIONSHIP.RelationCache:
									CACHE_RELATIONSHIP cacheRel = *(CACHE_RELATIONSHIP*)dataPtr;
									ulong cacheSize = cacheRel.CacheSize;
									if (cacheSize > largestCacheBytes)
									{
										largestCacheBytes = cacheSize;
									}
									break;
								default:
									break;
							}

							offset += size;
						}
					}
				}
				catch
				{ }
				finally
				{
					Marshal.FreeHGlobal(buffer);
				}
			}
		}
		catch
		{ }

		string cachePart = largestCacheBytes > 0UL ? FormatCache(largestCacheBytes) : "Cache N/A";

		StringBuilder sb = new(96);
		_ = sb.Append(brand);
		_ = sb.Append(" | ");
		_ = sb.Append(physicalCores.ToString(CultureInfo.InvariantCulture));
		_ = sb.Append(" Core / ");
		_ = sb.Append(logicalThreads.ToString(CultureInfo.InvariantCulture));
		_ = sb.Append(" Thread | ");
		_ = sb.Append(archText);
		_ = sb.Append(" | ");
		_ = sb.Append(baseClock);
		_ = sb.Append(" | ");
		_ = sb.Append(cachePart);
		_ = sb.Append(" | ");
		_ = sb.Append(packageCount.ToString(CultureInfo.InvariantCulture));
		_ = sb.Append(" Socket");

		return sb.ToString();
	}

	/// <summary>
	/// Formats bytes as MB or GB with one decimal, labeling as L3 if sufficiently large.
	/// </summary>
	private static string FormatCache(ulong bytes)
	{
		double mb = bytes / 1024.0 / 1024.0;
		string sizeText;
		if (mb >= 1024.0)
		{
			double gb = mb / 1024.0;
			sizeText = gb.ToString("0.#", CultureInfo.InvariantCulture) + " GB";
		}
		else
		{
			sizeText = mb.ToString("0.#", CultureInfo.InvariantCulture) + " MB";
		}

		return sizeText + " L3 Cache";
	}

	#endregion

	#region Extended RAM

	private static string GetPhysicalMemoryExtendedString()
	{
		string sizeText = GetAvailablePhysicalMemoryBytes();
		string? genSpeed = TryGetMemoryGenerationAndSpeed();
		if (string.IsNullOrEmpty(genSpeed))
		{
			return sizeText;
		}
		return sizeText + " | " + genSpeed;
	}

	/// <summary>
	/// Parses the SMBIOS (Type 17) structure to find the configured memory speed and generation.
	/// Returns the maximum configured speed found (e.g. "DDR4 3200 MT/s").
	/// Sources of truth: 7.18 Memory Device (Type 17) in the following PDF spec:
	/// https://www.dmtf.org/dsp/DSP0134
	/// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.9.0.pdf
	/// </summary>
	private static string? TryGetMemoryGenerationAndSpeed()
	{
		try
		{
			const uint ProviderRSMB = 0x52534D42; // 'RSMB' (Raw SMBIOS)
			uint tableSize = NativeMethods.GetSystemFirmwareTable(ProviderRSMB, 0, IntPtr.Zero, 0);
			if (tableSize == 0 || tableSize > 1_000_000U)
			{
				return null;
			}

			IntPtr buffer = Marshal.AllocHGlobal((int)tableSize);
			try
			{
				uint read = NativeMethods.GetSystemFirmwareTable(ProviderRSMB, 0, buffer, tableSize);
				if (read != tableSize || tableSize < 8)
				{
					return null;
				}

				// Raw SMBIOS header: first 8 bytes.
				// DWORD at offset 4 is table data length.
				// BYTE at offset 1 is Major Version, BYTE at offset 2 is Minor Version.
				uint smbiosDataLength = (uint)Marshal.ReadInt32(buffer, 4);
				int headerSize = 8;
				uint expectedEnd = (uint)headerSize + smbiosDataLength;
				if (expectedEnd > tableSize)
				{
					// Clamp if firmware reports inconsistent size.
					smbiosDataLength = tableSize - (uint)headerSize;
					expectedEnd = (uint)headerSize + smbiosDataLength;
				}

				int pos = headerSize;
				int max = (int)expectedEnd;

				// We'll collect the chosen speed for each populated slot, then pick the max.
				List<uint> chosenSpeeds = new(8);
				int detectedMemoryType = 0; // 0 = Unknown

				// Type 17 (Memory Device) field offsets (Decimal):
				const int OffsetMemoryType = 18; // (0x12): Memory Type (BYTE)
				const int OffsetSpeed = 21; // (0x15): Speed (WORD)
				const int OffsetConfiguredSpeed = 32; // (0x20): Configured Memory Clock Speed (WORD)
				const int OffsetExtendedSpeed = 57; // (0x39): Extended Speed (DWORD) - SMBIOS 3.3+
				const int OffsetExtendedConfiguredSpeed = 61; // (0x3D): Extended Configured Memory Speed (DWORD) - SMBIOS 3.3+

				while (pos + 4 <= max)
				{
					byte structureType = Marshal.ReadByte(buffer, pos);
					byte length = Marshal.ReadByte(buffer, pos + 1);

					if (length < 4 || pos + length > max)
					{
						break;
					}

					if (structureType == 17)
					{
						ushort baseSpeed = 0;
						ushort configuredSpeed = 0;
						uint extendedSpeed = 0;
						uint extendedConfiguredSpeed = 0;
						byte currentType = 0;

						// 0. Memory Type (Offset 18)
						// We need length >= 19 to read byte at offset 18 safely
						if (length >= OffsetMemoryType + 1)
						{
							currentType = Marshal.ReadByte(buffer, pos + OffsetMemoryType);
						}

						// 1. Speed (Offset 21)
						if (length >= OffsetSpeed + 2)
						{
							baseSpeed = (ushort)Marshal.ReadInt16(buffer, pos + OffsetSpeed);
						}

						// 2. Configured Clock Speed (Offset 32)
						if (length >= OffsetConfiguredSpeed + 2)
						{
							configuredSpeed = (ushort)Marshal.ReadInt16(buffer, pos + OffsetConfiguredSpeed);
						}

						// 3. Extended Speed (Offset 57) - DWORD
						if (length >= OffsetExtendedSpeed + 4)
						{
							extendedSpeed = (uint)Marshal.ReadInt32(buffer, pos + OffsetExtendedSpeed);
						}

						// 4. Extended Configured Speed (Offset 61) - DWORD
						if (length >= OffsetExtendedConfiguredSpeed + 4)
						{
							extendedConfiguredSpeed = (uint)Marshal.ReadInt32(buffer, pos + OffsetExtendedConfiguredSpeed);
						}

						// Logic to determine actual speed for this stick:
						// "0" usually means unknown.
						// "0xFFFF" means the value is too large for WORD and resides in the Extended field.
						uint selected = 0;

						// Priority 1: Extended Configured Speed (if ConfiguredSpeed == 0xFFFF)
						if (configuredSpeed == 0xFFFF && extendedConfiguredSpeed != 0)
						{
							selected = extendedConfiguredSpeed;
						}
						// Priority 2: Standard Configured Speed
						else if (configuredSpeed != 0 && configuredSpeed != 0xFFFF)
						{
							selected = configuredSpeed;
						}
						// Priority 3: Extended Speed (if BaseSpeed == 0xFFFF)
						else if (baseSpeed == 0xFFFF && extendedSpeed != 0)
						{
							selected = extendedSpeed;
						}
						// Priority 4: Standard Base Speed
						else if (baseSpeed != 0 && baseSpeed != 0xFFFF)
						{
							selected = baseSpeed;
						}

						// Filter out plausible garbage or "Unknown" (0)
						// MT/s is typically >= 300 for DDR1 and up to ~10000+ for future DDR5/6.
						if (selected >= 300 && selected <= 30000)
						{
							chosenSpeeds.Add(selected);

							// If we found a valid speed, capture the memory type of this stick.
							// We prefer a known type (e.g. DDR4/5) over unknown.
							if (currentType > 0 && detectedMemoryType == 0)
							{
								detectedMemoryType = currentType;
							}
							else if (currentType > detectedMemoryType)
							{
								// Heuristic: if we have mixed types (unlikely), pick the "newer" one (higher value typically)
								// or just stick to the first found.
								detectedMemoryType = currentType;
							}
						}
					}

					// Advance to next structure.
					// Structures are double-null terminated (formatted section + string set).
					int stringStart = pos + length;
					int scan = stringStart;
					while (scan < max - 1)
					{
						byte b0 = Marshal.ReadByte(buffer, scan);
						byte b1 = Marshal.ReadByte(buffer, scan + 1);
						if (b0 == 0 && b1 == 0)
						{
							scan += 2; // Skip the two nulls
							break;
						}
						scan++;
					}

					// Safely advance 'pos'. If 'scan' didn't move or went out of bounds, force break.
					if (scan <= pos || scan > max)
					{
						break;
					}
					pos = scan;
				}

				if (chosenSpeeds.Count == 0)
				{
					return null;
				}

				// Return the maximum speed found among all populated slots.
				uint maxSpeed = 0;
				foreach (uint val in chosenSpeeds)
				{
					if (val > maxSpeed)
					{
						maxSpeed = val;
					}
				}

				string typeString = detectedMemoryType switch
				{
					0x18 => "DDR3",
					0x1A => "DDR4",
					0x1B => "LPDDR3",
					0x1C => "LPDDR4",
					0x22 => "DDR5",
					0x23 => "LPDDR5",
					_ => string.Empty
				};

				string speedString = maxSpeed.ToString(CultureInfo.InvariantCulture) + " MT/s";

				if (!string.IsNullOrEmpty(typeString))
				{
					return $"{typeString} {speedString}";
				}

				return speedString;
			}
			finally
			{
				Marshal.FreeHGlobal(buffer);
			}
		}
		catch
		{
			return null;
		}
	}

	#endregion

	#region OS Info

	/// <summary>
	/// Retrieves OS details such as Name, Edition, Display Version, and Build Number.
	/// </summary>
	/// <returns></returns>
	private static string GetOsDetailsString()
	{
		try
		{
			using RegistryKey? key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
			if (key is null)
			{
				return "Unknown OS";
			}

			string installationType = key.GetValue("InstallationType") as string ?? "Client";
			string currentBuild = key.GetValue("CurrentBuild") as string ?? "0";

			if (installationType.Contains("Server", StringComparison.OrdinalIgnoreCase))
			{
				string productName = key.GetValue("ProductName") as string ?? "Windows Server";
				return $"{productName} | Build {currentBuild}";
			}
			else
			{
				string editionId = key.GetValue("EditionID") as string ?? "Unknown";
				string displayVersion = key.GetValue("DisplayVersion") as string ?? "Unknown";
				return $"Microsoft Windows | {editionId} | Version {displayVersion} | Build {currentBuild}";
			}
		}
		catch
		{
			return "Unknown OS";
		}
	}

	#endregion

	public void Dispose()
	{
		_temperatureSampler?.Dispose();
		_temperatureSampler = null;
	}

}
